package com.segersten.othello.model;

import java.io.Serializable;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

import com.google.common.collect.Collections2;

public class DefaultBoard implements Serializable, Board
{

    public static int HUGE_SIZE = 12;
    public static int LARGE_SIZE = 10;
    public static int NORMAL_SIZE = 8;
    public static int SMALL_SIZE = 6;
    public static int TINY_SIZE = 4;

    private Color[][] colors;
    private Set<Brick> bricks;
    private Set<Point> boundaryPoints;
    private Set<Point> blackAvailablePoints;
    private Set<Point> whiteAvailablePoints;
    List<RoundAction> roundActions;
    private int size;
    private NextColorStrategy excpectedNexColor;

    public static DefaultBoard createDisabledTurnBoard(int size)
    {
        return new DefaultBoard(size, new DisabledNextColorStrategy());
    }

    public static DefaultBoard createBoard(int size)
    {
        return new DefaultBoard(size, new DefaultNextColorStrategy());
    }

    public static Board createBoard(DefaultBoard board)
    {
        return new DefaultBoard(board);
    }

    public static Board createDefaultBoard(int size)
    {
        return new DefaultBoard(NORMAL_SIZE, new DefaultNextColorStrategy());
    }

    private DefaultBoard()
    {

    }

    private DefaultBoard(DefaultBoard other)
    {
        this.size = other.size;
        colors = new Color[size][size];
        for (int i = 0; i < other.size; i++)
        {
            for (int j = 0; j < other.size; j++)
            {
                colors[i][j] = other.colors[i][j];
            }
        }
        bricks = new LinkedHashSet<Brick>(other.bricks);
        boundaryPoints = new LinkedHashSet<Point>(other.boundaryPoints);
        blackAvailablePoints = new LinkedHashSet<Point>();
        whiteAvailablePoints = new LinkedHashSet<Point>();
        roundActions = new LinkedList<RoundAction>(other.roundActions);
        excpectedNexColor = other.excpectedNexColor;
    }

    private DefaultBoard(int size, NextColorStrategy excpectedNexColor)
    {
        assertEvenSize(size);
        this.size = size;
        colors = new Color[size][size];
        bricks = new LinkedHashSet<Brick>();
        boundaryPoints = new LinkedHashSet<Point>();
        blackAvailablePoints = new LinkedHashSet<Point>();
        whiteAvailablePoints = new LinkedHashSet<Point>();
        roundActions = new LinkedList<RoundAction>();
        int point = size / 2;
        putBrickInternal(RoundAction.createMove(new Point(point - 1, point - 1), Color.BLACK));
        putBrickInternal(RoundAction.createMove(new Point(point, point), Color.BLACK));
        putBrickInternal(RoundAction.createMove(new Point(point - 1, point), Color.WHITE));
        putBrickInternal(RoundAction.createMove(new Point(point, point - 1), Color.WHITE));
        this.excpectedNexColor = excpectedNexColor;
        if (whiteAvailablePoints.isEmpty() && blackAvailablePoints.isEmpty())
        {
            stopGame();
        }
    }

    public Set<Point> getBoundary()
    {
        return this.boundaryPoints;
    }

    /* (non-Javadoc)
     * @see com.segersten.othello.model.Board#getRoundActions()
     */
    @Override
    public List<RoundAction> getRoundActions()
    {
        return this.roundActions;
    }

    public int getRoundNumber()
    {
        return roundActions.size();
    }

    /* (non-Javadoc)
     * @see com.segersten.othello.model.Board#getSize()
     */
    @Override
    public int getSize()
    {
        return size;
    }

    /* (non-Javadoc)
     * @see com.segersten.othello.model.Board#getNextColor()
     */
    @Override
    public Color getNextColor()
    {
        return excpectedNexColor.getExpectedColor();
    }

    public int getScore(Color positiveColor)
    {
        // TODO : Parallelize using fork/join pattern
        BoardScoreFunction func = new BoardScoreFunction(positiveColor, size);
        for (Brick brick : bricks)
        {
            func.apply(brick);
        }
        return func.getScoreSum();
    }

    public String encodeBricks()
    {
        StringBuilder sb = new StringBuilder();
        Iterator<RoundAction> iter = roundActions.iterator();
        if (iter.hasNext())
        {
            RoundAction brick = iter.next();
            sb.append(brick.toShortString());
            while (iter.hasNext())
            {
                sb.append("," + iter.next().toShortString());
            }
        }
        return sb.toString();
    }

    /* (non-Javadoc)
     * @see com.segersten.othello.model.Board#putBrick(com.segersten.othello.model.RoundAction)
     */
    @Override
    public void putBrick(RoundAction brick) throws IllegalActionException
    {
        excpectedNexColor.nextRoundAction(brick.getColor(), whiteAvailablePoints, blackAvailablePoints);
        if (brick.isPass())
        {
            roundActions.add(brick);
            return;
        }
        assertAvailablePoint(brick);
        List<Point> flipped = putBrickInternal(brick);
        if (flipped.isEmpty())
        {
            throw new IllegalStateException("At least one should have flipped at brick " + brick);
        }
        if (whiteAvailablePoints.isEmpty() && blackAvailablePoints.isEmpty())
        {
            stopGame();
        }
        roundActions.add(brick);
    }

    /* (non-Javadoc)
     * @see com.segersten.othello.model.Board#stopGame()
     */
    @Override
    public void stopGame()
    {
        blackAvailablePoints = Collections.EMPTY_SET;
        whiteAvailablePoints = Collections.EMPTY_SET;
        excpectedNexColor = excpectedNexColor.stop();
    }
 
    @Override
    public Set<Point> getAvailblePoints(Color color)
    {
        if (color==null) {
            return Collections.EMPTY_SET;
        }
        return color.equals(Color.WHITE) ? getAvailblePointsWhite() : getAvailblePointsBlack();
    }

    @Override
    public int getColorCount(Color color)
    {
        return Collections2.filter(bricks, new EqualColorPredicate(color)).size();
    }

    public Color getColor(Point point)
    {
        assertNotNull(point, "point");
        return colors[point.getX()][point.getY()];
    }

    public String toString()
    {

        return toString(Color.WHITE);
    }

    public String toString(Color color)
    {
        String str = "";
        for (int y = 0; y < size; y++)
        {
            for (int x = 0; x < size; x++)
            {
                char p = '.';
                Point point = new Point(x, y);
                Color c = getColor(point);
                if (c != null)
                {
                    p = c.equals(Color.WHITE) ? 'w' : 'b';
                }
                if (getAvailblePoints(color).contains(point))
                {
                    p = '*';
                }/*
                  * else if (boundaryPoints.contains(point)) { p='#'; }
                  */
                str += p;
            }
            str += "\n";
        }

        return "board=\n" + str + "]";
    }

    public boolean testIsOpponentColorEndedWithMine(Color color, Point point, Point nextPoint, int deltaX, int deltaY)
    {
        return isOpponentColorEndedWithMineInitial(color, point) && isOpponentColorEndedWithMine(color, point, nextPoint, deltaX, deltaY);
    }

    // /////////// PRIVATE ////////////////
    // ////////////////////////////////////
    private void assertNotNull(Object obj, String string)
    {
        if (obj == null)
        {
            throw new NullPointerException("Cannot be null " + string);
        }
    }

    private List<Point> putBrickInternal(RoundAction brick)
    {
        Point point = brick.getPoint();
        Color color = brick.getColor();
        // System.out.println("Pre-Put brick "+point +" " + getColor(point));
        setPoint(point, color);
        // System.out.println("Put brick "+point +" " + getColor(point));
        // System.out.println(this.toString());
        // recurcive flip
        List<Point> flipped = new LinkedList<Point>();

        // new boundary
        for (int deltaX = -1; deltaX < 2; deltaX++)
        {
            for (int deltaY = -1; deltaY < 2; deltaY++)
            {
                Point pint = new Point(point.getX() + deltaX, point.getY() + deltaY);
                if (isOutOfBound(pint))
                {
                    continue;
                }
                // System.out.println("* Direction dx="+deltaX +" dy=" + deltaY
                // + " -->"+pint + " " + getColor(pint));

                if (isEmpty(pint))
                {
                    boundaryPoints.add(pint);
                } else
                {
                    boundaryPoints.remove(pint);

                    if ((deltaX == 0 && deltaY == 0) == false)
                    {
                        List<Point> flippedPart = new LinkedList<Point>();
                        flippedPart = flip(color, pint, deltaX, deltaY, flippedPart);
                        flipped.addAll(flippedPart);
                    }
                }
            }
        }

        // verify & update available lists
        whiteAvailablePoints = updateAvailability(Color.WHITE, boundaryPoints);
        blackAvailablePoints = updateAvailability(Color.BLACK, boundaryPoints);

        return flipped;
    }

    private void setPoint(Point point, Color color)
    {
        Brick brick = new Brick(point, color);
        if (bricks.contains(brick))
        {
            bricks.remove(brick);
        }
        bricks.add(brick);
        colors[point.getX()][point.getY()] = color;
    }

    private Set<Point> updateAvailability(Color color, Set<Point> availableSource)
    {
        Set<Point> availableDest = new LinkedHashSet<Point>();

        for (Point point : availableSource)
        {
            if (isAvailable(color, point))
            {
                availableDest.add(point);
            }
        }
        return availableDest;
    }

    private List<Point> flip(Color color, Point point, int deltaX, int deltaY, List<Point> flippedPart)
    {
        Point pint = new Point(point.getX() + deltaX, point.getY() + deltaY);
        // System.out.println("Direction dx="+deltaX +" dy=" + deltaY);
        if (isOutOfBound(pint))
        {
            flippedPart.clear();
            return flippedPart;
        }
        // System.out.println("Checking1 "+point +" " + getColor(point));
        // System.out.println("Checking2 "+pint +" " + getColor(pint));
        if (isEmpty(pint))
        {
            flippedPart.clear();
            return flippedPart;
        }

        if (getColor(point).equals(color) == false)
        {
            flippedPart.add(point); // flip this one
        } else if (flippedPart.size() == 0)
        {
            return flippedPart; // if first brick is your own stop the recursion
        }

        if (getColor(pint).equals(color) == true)
        { // recursion is done, do the
          // flip of colors
            for (Point p : flippedPart)
            {
                setPoint(p, color);
            }
            return flippedPart; // done!
        }

        return flip(color, pint, deltaX, deltaY, flippedPart);
    }

    private boolean isEmpty(Point p)
    {
        return colors[p.getX()][p.getY()] == null;
    }

    public boolean testIsAvailable(Color c, Point p)
    {
        return isAvailable(c, p);
    }

    private boolean isAvailable(Color color, Point point)
    {
        for (int deltaX = -1; deltaX < 2; deltaX++)
        {
            for (int deltaY = -1; deltaY < 2; deltaY++)
            {
                if (deltaX == 0 && deltaY == 0)
                {
                    continue;
                }
                Point pint = new Point(point.getX() + deltaX, point.getY() + deltaY);
                Point nextPint = new Point(pint.getX() + deltaX, pint.getY() + deltaY);
                if (isOpponentColorEndedWithMineInitial(color, pint) && isOpponentColorEndedWithMine(color, pint, nextPint, deltaX, deltaY))
                {
                    return true;
                }
            }
        }
        return false;
    }

    private boolean isOpponentColorEndedWithMineInitial(Color color, Point point)
    {
        if (isOutOfBound(point))
        {
            return false;
        }
        if (isEmpty(point))
        {
            return false;
        }
        return getColor(point).equals(color) == false;
    }

    private boolean isOpponentColorEndedWithMine(Color color, Point point, Point nextPoint, int deltaX, int deltaY)
    {
        if (isOutOfBound(point))
        {
            return false;
        }
        if (isEmpty(point))
        {
            return false;
        }
        if (isOutOfBound(nextPoint))
        {
            return false;
        }
        if (isEmpty(nextPoint))
        {
            return false;
        }

        if (getColor(point).equals(color) == false && getColor(nextPoint).equals(color) == true)
        {
            return true;
        }

        Point nextPint = new Point(nextPoint.getX() + deltaX, nextPoint.getY() + deltaY);
        return isOpponentColorEndedWithMine(color, nextPoint, nextPint, deltaX, deltaY);
    }

    private boolean isOutOfBound(Point p)
    {
        if (p.getX() < 0)
            return true;
        if (p.getX() >= size)
            return true;
        if (p.getY() < 0)
            return true;
        if (p.getY() >= size)
            return true;

        return false;
    }

    private void assertEvenSize(int size)
    {
        if ((size / 2) * 2 != size)
        {
            throw new IllegalArgumentException("The size must be an even number, was: " + size);
        }
    }

    private void assertAvailablePoint(RoundAction brick) throws IllegalActionException
    {
        Set<Point> availablePoints = getAvailblePoints(brick.getColor());
        if (availablePoints.contains(brick.getPoint()) == false)
        {
            throw new IllegalActionException("Tried to put a brick on a non-available point: " + brick);
        }
    }

    private Set<Point> getAvailblePointsWhite()
    {
        return whiteAvailablePoints;
    }

    private Set<Point> getAvailblePointsBlack()
    {
        return blackAvailablePoints;
    }

}